﻿// (c) Norbert Huffschmid
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.

using System;
using System.ComponentModel;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Data;
using System.Collections.ObjectModel;

namespace SpinDotters.Core
{
    public class NumericUpDown : UpDownBase
    {
        /// <summary>
        /// Type converter for support of decimal input in XAML files
        /// </summary>
        public class CustomDecimalConverter : TypeConverter
        {
            // inspired by http://forums.silverlight.net/p/65691/161424.aspx
            public override Object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, Object value)
            {
                if (value is String)
                {
                    // dot-syntax is mandatory for all developer machines, e.g. 0.5 instead of 0,5
                    decimal parsedValue = Decimal.Parse((string)value, new CultureInfo("en-US"));
                    return parsedValue;
                }
                else
                    throw new InvalidCastException("Invalid value type");
            }

            public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
            {
                if (sourceType == typeof(String))
                    return true;
                else
                    return false;
            }
        }

        private class ValueToSelectedIndexConverter : IValueConverter
        {
            public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
            {
                NumericUpDown nud = (NumericUpDown) parameter;
                decimal _value = (decimal)value;
                int selectedIndex = (int)((_value - nud.Minimum) / nud.increment);
                return selectedIndex;
            }

            public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
            {
                NumericUpDown nud = (NumericUpDown)parameter;
                int selectedIndex = (int)value;

                if (selectedIndex == -1)
                    return nud.Minimum;
                else
                    return (nud.Minimum + selectedIndex * nud.increment);
            }
        }


        private const decimal DEFAULT_MIN = 0;
        private const decimal DEFAULT_MAX = 9;
        private const decimal DEFAULT_VALUE = 0;
        private const int DEFAULT_DECIMAL_PLACES = 0;
        private const int DEFAULT_INCREMENT = 1;
        private const bool DEFAULT_SHOW_ZERO_AS_BLANK = false;

        private int decimalPlaces = DEFAULT_DECIMAL_PLACES;
        private decimal increment = DEFAULT_INCREMENT;

        
        /// <summary>
        /// Event fired when spinning is done and value has changed.
        /// </summary>
        [Category("SpinDotters")]
        public event EventHandler ValueChanged;


        /// <summary>
        /// Identifies the Minimum dependency property.
        /// </summary>
        public static readonly DependencyProperty MinimumProperty =
            DependencyProperty.Register(
                "Minimum",
                typeof(decimal),
                typeof(NumericUpDown),
                new PropertyMetadata(DEFAULT_MIN, new PropertyChangedCallback(OnPropertyChanged))
            );

        /// <summary>
        /// Gets or sets the minimum allowed value for the control.
        /// </summary>
        [Category("SpinDotters")]
        [TypeConverter(typeof(CustomDecimalConverter))]
        public decimal Minimum
        {
            get { return (decimal)GetValue(MinimumProperty); }
            set
            {
                SetValue(MinimumProperty, value);

                // follow logic of System.Windows.Forms.NumericUpDown
                // http://msdn.microsoft.com/de-de/library/system.windows.forms.numericupdown.minimum.aspx

                if (value > this.Maximum)
                    this.Maximum = value;

                if (this.Value < value)
                    SetCurrentDependencyPropertyValue(NumericUpDown.ValueProperty, value);
            }
        }

        /// <summary>
        /// Identifies the Maximum dependency property.
        /// </summary>
        public static readonly DependencyProperty MaximumProperty =
            DependencyProperty.Register(
                "Maximum",
                typeof(decimal),
                typeof(NumericUpDown),
                new PropertyMetadata(DEFAULT_MAX, new PropertyChangedCallback(OnPropertyChanged))
            );

        /// <summary>
        /// Gets or sets the maximum allowed value for the control.
        /// </summary>
        [Category("SpinDotters")]
        [TypeConverter(typeof(CustomDecimalConverter))]
        public decimal Maximum
        {
            get { return (decimal)GetValue(MaximumProperty); }
            set
            {
                SetValue(MaximumProperty, value);

                // follow logic of System.Windows.Forms.NumericUpDown
                // http://msdn.microsoft.com/de-de/library/system.windows.forms.numericupdown.maximum.aspx

                if (value < this.Minimum)
                    this.Minimum = value;

                if (this.Value > value)
                    SetCurrentDependencyPropertyValue(NumericUpDown.ValueProperty, value);
            }
        }

        /// <summary>
        /// Gets or sets the number of decimal places.
        /// </summary>
        [Category("SpinDotters")]
        public int DecimalPlaces
        {
            get { return this.decimalPlaces; }
            set
            {
                if ((value < 0) || (value > 99))
                    throw new ArgumentOutOfRangeException("Invalid value for decimal places!");

                this.decimalPlaces = value;
                FillItems();
            }
        }

        /// <summary>
        /// Gets or sets the value to increment or decrement.
        /// </summary>
        [Category("SpinDotters")]
        [TypeConverter(typeof(CustomDecimalConverter))]
        public decimal Increment
        {
            get { return this.increment; }
            set
            {
                if (value <= 0)
                    throw new ArgumentOutOfRangeException("Invalid value for increment!");

                this.increment = value;
                FillItems();
            }
        }

        /// <summary>
        /// Identifies the Value dependency property.
        /// </summary>
        public static readonly DependencyProperty ValueProperty =
            DependencyProperty.Register(
                "Value",
                typeof(decimal),
                typeof(NumericUpDown),
                new PropertyMetadata(DEFAULT_VALUE, new PropertyChangedCallback(OnValueChanged))
            );

        /// <summary>
        /// Gets or sets the value assigned to the control.
        /// </summary>
        [Category("SpinDotters")]
        [TypeConverter(typeof(CustomDecimalConverter))]
        public decimal Value
        {
            get { return (decimal)GetValue(ValueProperty); }

            set
            {
                // follow logic of System.Windows.Forms.NumericUpDown
                // http://msdn.microsoft.com/de-de/library/system.windows.forms.numericupdown.value.aspx

                if (value < this.Minimum)
                    throw new ArgumentOutOfRangeException("The assigned value is less than the Minimum property value.");

                if (value > this.Maximum)
                    throw new ArgumentOutOfRangeException("The assigned value is greater than the Maximum property value.");

                SetValue(ValueProperty, value);
            }
        }

        /// <summary>
        /// Identifies the ShowZeroAsBlank dependency property.
        /// </summary>
        public static readonly DependencyProperty ShowZeroAsBlankProperty =
            DependencyProperty.Register(
                "ShowZeroAsBlank",
                typeof(bool),
                typeof(NumericUpDown),
                new PropertyMetadata(DEFAULT_SHOW_ZERO_AS_BLANK, new PropertyChangedCallback(OnPropertyChanged)));

        /// <summary>
        /// Determines whether zero renders as digit zero or blank.
        /// </summary>
        [Category("SpinDotters")]
        public bool ShowZeroAsBlank
        {
            get { return (bool)GetValue(ShowZeroAsBlankProperty); }
            set
            {
                SetValue(ShowZeroAsBlankProperty, value);

                FillItems();
            }
        }

        /// <summary>
        /// Default constructor
        /// </summary>
        public NumericUpDown()
        {
            FillItems();

            Binding valueBinding = new Binding("ValueProperty");
            valueBinding.Source = this;
            valueBinding.Path = new PropertyPath("Value");
            valueBinding.Mode = BindingMode.TwoWay;
            valueBinding.Converter = new ValueToSelectedIndexConverter();
            valueBinding.ConverterParameter = this;
            this.SetBinding(NumericUpDown.SelectedIndexProperty, valueBinding);
        }

        protected virtual void OnValueChanged()
        {
            if (ValueChanged != null)
                ValueChanged(this, EventArgs.Empty);
        }

        /// <summary>
        /// Determine index from key
        /// </summary>
        /// <param name="key">Key parameter as delivered in KeyEventArgs</param>
        /// <returns>Index of matching item or -1 in case of no matching item</returns>
        protected override int DetermineIndexFromKey(Key key)
        {
            if ((this.decimalPlaces != 0) ||
                (this.increment != 1) ||
                (this.Minimum < 0) ||
                (Decimal.Round(this.Minimum, 0) != this.Minimum) ||
                (this.Maximum > 9))
                // no item matching under these conditions
                return -1;

            int keyValue = -1;

            switch (key)
            {
                case Key.D1: keyValue = 1; break;
                case Key.D2: keyValue = 2; break;
                case Key.D3: keyValue = 3; break;
                case Key.D4: keyValue = 4; break;
                case Key.D5: keyValue = 5; break;
                case Key.D6: keyValue = 6; break;
                case Key.D7: keyValue = 7; break;
                case Key.D8: keyValue = 8; break;
                case Key.D9: keyValue = 9; break;
                case Key.D0: keyValue = 0; break;

                case Key.Delete:
                case Key.Back:
                case Key.Space:
                    {
                        if (this.ShowZeroAsBlank)
                            keyValue = 0;

                        break;
                    }
            }

            if ((keyValue >= 0) && (keyValue >= this.Minimum) && (keyValue <= this.Maximum))
                return (keyValue - (int)this.Minimum);

            return -1;
        }

        private void FillItems()
        {
            Items.Clear();

            for (decimal d = this.Minimum; d <= this.Maximum; d += this.increment)
            {
                string item;

                if ((d == 0) && this.ShowZeroAsBlank)
                    item = " ";
                else
                {
                    // avoid thousands separator, e.g. when years are shown in datepicker
                    NumberFormatInfo nfi = new CultureInfo("en-US").NumberFormat;
                    nfi.NumberGroupSeparator = "";
                    item = d.ToString("N" + this.decimalPlaces, nfi);
                }

                this.Items.Add(item);
            }
        }

        private static void OnPropertyChanged(DependencyObject dependencyObject,
            DependencyPropertyChangedEventArgs e)
        {
            NumericUpDown snud = (NumericUpDown)dependencyObject;

            snud.FillItems();
        }

        private static void OnValueChanged(DependencyObject dependencyObject,
            DependencyPropertyChangedEventArgs e)
        {
            NumericUpDown snud = (NumericUpDown)dependencyObject;

            snud.OnValueChanged();
        }
    }
}
